﻿using FileManagerSystem.DbInfos;
using FileManagerSystem.Extend;
using FileManagerSystem.Helper;
using FileManagerSystem.Models;
using FileManagerSystem.Repository;
using FileManagerSystem.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace FileManagerSystem.Controllers
{
    /// <summary>
    /// 文件上传接口
    /// </summary>
    public class FileUploadController : ApiBaseController
    {
        /// <summary>
        /// 原文件Service对象
        /// </summary>
        [Autowired]
        private IFileService _fileService { get; set; }

        /// <summary>
        /// 映射文件Service对象
        /// </summary>
        [Autowired]
        private IReflectFileService _reflectfileService { get; set; }

        /// <summary>
        /// 目录Service对象
        /// </summary>
        [Autowired]
        private IDirectoryService _directoryService { get; set; }

        /// <summary>
        /// 文件夹上传时，创建虚拟顶级文件夹，方便后续通过顶级文件夹 Guid 拉取其下的所有文件
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<IActionResult> CreateNewDir(DirectoryDetail model)
        {
            var dirName = model.Name.Trim().Replace("/", "").Replace(@"\", "");//去除前后空白以及斜杠和反斜杠
            if (dirName.Contains("string") || dirName.Contains("undefined") || dirName.IsNullOrWhiteSpace())
                throw new Exception($"请检查参数是否正确提交！");

            var newDirGUID = await _directoryService.Insert(new DirectoryInf
            {
                AbstractDirPath = dirName + @"\",
                Name = dirName.Trim()
            });
            return Json(newDirGUID.AsGuid.ToString());
        }

        /// <summary>
        /// 文件分片上传接口
        /// </summary>
        ///<remarks>
        ///         注：如果分片大小与总分片大小都等于1，则第一片文件上传完毕后会就会直接合并文件并返回成功或秒传成功
        ///         
        /// 代码实现参考如下（javascript示例，仅做业务逻辑参考，具体实现请根据所用语言自行实现！）：
        ///         
        ///         // 设置基础数值 chunkSize默认分片大小1MB
        ///         var chunkSize = 1024*1024;
        ///         // 这个数据需要调用CreateNewDir获取，如果是单文件上传这个值可以为空
        ///         var topDirGuid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
        ///         
        ///         // 单文件多文件均可用循环，实现效果是一样的
        ///         //      files       上传控件捕获到的文件集合
        ///         for(file in files){
        ///             // 文件信息处理，如计算第一片文件的md5等，总共多少切片maxChunk，文件所属路径path等等，需要前端自行实现！！！
        ///             let maxChunk = Math.ceil(file.size / chunkSize);
        ///             let path = files.files[i].webkitRelativePath.split('/')[0];
        ///             if(!path) path="";//防止没有路径生成undefined，没有子目录可以传空
        ///             
        ///             // 第一片上传自此开始
        ///             fileUpload(file, 1, "", 0, maxChunk, md5, path，topDirGuid);
        ///         }
        ///         
        ///         //上传接口
        ///         //      一些参数解释：
        ///         //      file        完整的文件信息，
        ///         //      index       第几个分片       如果未来有断点续传功能，index可能会跳变，需要注意一下
        ///         //      maxChunk    总分片数
        ///         //      md5         必须前端生成      在第一片请求时需外部传入，第二片开始后会在异步调用的递归中自动传入
        ///         //      path        文件所属目录  
        ///         //      topDirGuid  可以为空        为空表示是单文件直接上传（根据业务需求来，如果是文件夹上传这里必须要有值）
        ///         //      lackedChunks 缺少的分片      仅断点续传会用此参数  
        ///         function fileUpload(file, index, session, statu, maxChunk, md5, path, topDirGuid, lackedChunks){
        ///             //生成表单，表单内容参考 ChunkFileUploadDetail 实体
        ///             let formData = new FormData();
        ///             formData.append("statu", statu);
        ///             formData.append("file",
        ///                 file.slice((index-1) * chunkSize, index * chunkSize)//将文件进行分段
        ///             );
        ///             formData.append("topDirGuid", topDirGuid);
        ///             formData.append("session", session);
        ///             formData.append("name", file.name);
        ///             formData.append("path", fpath);
        ///             formData.append("chunkSize", chunkSize);
        ///             formData.append("fileSize", file.size);
        ///             formData.append("chunk", index);
        ///             formData.append("maxChunk", maxChunk);
        ///             formData.append("md5", md5);
        ///             
        ///             $.ajax({
        ///                 url: uploadUrl,//上传地址就是此接口，这里仅做代码示例，具体以实际业务为准
        ///                 type: 'POST',
        ///                 cache: false,
        ///                 data: formData,
        ///                 processData: false,
        ///                 contentType: false,
        ///             }).done(function (res) {
        ///                  if (res.Statu == 1)//正常分片上传逻辑  
        ///                  {
        ///                      index++;
        ///                      let statu = res.Statu;
        ///                      if (index == maxChunk) statu = 3;//如果是最后一片，设置为3，很重要！
        ///                      
        ///                      // 递归调用函数自身，因为ajax是异步，所以for循环把ajax执行完毕后，浏览器不会等待
        ///                      // 如此做可以避免浏览器假死，当后台拿到返回值时，浏览器才会继续执行后续分片上传
        ///                      // 禁止使用ajax的同步上传方法，请务必在回调中执行下个分片上传！！！
        ///                      // 如果不如此做，可能会导致上传的分片丢失，从而文件合并失败！！！
        ///                      // 禁止使用while循环分片的方式上传！！！
        ///                      // 后台会返回会话标识 即res.Session
        ///                      fileUpload(file, index, res.Session, statu, maxChunk, md5, path, topDirGuid);
        ///                  }
        ///                  else if (res.Statu == 2)//断点续传逻辑  
        ///                  {
        ///                      // 断点续传时，仅会在第一片检查缺少分片集合，前端需要自己处理分片依次补传                 
        ///                      // res.LackedChunks 缺失的分片集合
        ///                      if(res.LackedChunks.length > 0)
        ///                         lackedChunks = res.LackedChunks;
        ///                      index = res.LackedChunks[0];//下个上传缺失分片的第一个分片
        ///                      lackedChunks.shift();//在缺失分片集合中，移除第一个分片信息（因为马上就要上传了，下个递归过程不需要了）
        ///                      
        ///                      let statu = res.Statu;
        ///                      if (index == lackedChunks[lackedChunks.length-1]) statu = 3;//如果是最后一片，设置为3，很重要！
        ///                      
        ///                      fileUpload(file, index, res.Session, statu, maxChunk, md5, path, topDirGuid, lackedChunks);
        ///                  }
        ///                  else if (res.Statu == 3)//上传成功
        ///                  {
        ///                      // 刷新前端进度条等等信息，请自行实现
        ///                      console.log("上传成功", res);
        ///                  }
        ///                  else if (res.Statu == 4)//秒传成功提示
        ///                  {
        ///                      // 刷新前端进度条等等信息，请自行实现
        ///                      console.log("文件已存在，秒传成功", res);
        ///                  }
        ///              }).fail(function (res) {
        ///                 // 失败提示
        ///                 console.log("文件上传失败", res);
        ///              });
        ///         }
        /// 
        /// 完整上传业务流程（请务必仔细阅读，完全理解后再写上传业务！！！）：
        /// 
        ///         〇.(文件夹上传前置步骤)       注：单文件上传可跳过本步骤
        ///            如果是文件夹上传，上传分片前，先请求CreateNewDir()接口，获取文件夹guid标识，后面上传的只要是该文件夹下的文件，即使是嵌套文件
        ///            如果不是文件夹上传，跳过此步骤。
        /// 
        ///         ①.(前端发起第一次请求)
        ///            每个大文件上传时，都需要前端对文件切片，form表单中: 
        ///                statu设置为初始0；
        ///                md5设置为第一片md5值；
        ///                session保留空；
        ///                path路径 以及 dirGuid虚拟目录Guid 等其他参数根据实际情况设置
        ///                     
        ///         ②.(后台返回状态Statu，会话Session，文件名Name，Md5，缺少的区块LackedChunks(如果有的话))
        ///            第一片文件上传成功，后台会返回前端Statu状态，根据业务状态，执行对应逻辑
        ///            根据后台返回的状态，主要有四个逻辑：
        ///                Statu为1，继续下个分片上传；
        ///                Statu为2，上次没传完需要断点续传（同时返回缺失分片信息）；
        ///                Statu为3，文件第一片就直接上传成功；
        ///                Statu为4，秒传成功标识（前端直接跳过当前文件上传）
        ///                     
        ///         ③.(前端根据②返回的状态继续发起请求，或停止请求)
        ///            前端继续后续的分片上传，但如果②中状态Statu为4(秒传)，则直接跳过本文件上传并显示秒传成功
        ///            注意如果不是秒传，此时前端提交的form表单中：
        ///                statu设置为后台返回的状态Statu；
        ///                md5设置为后台返回的Md5；
        ///                session设置为后台返回的会话Session；
        ///                path路径 以及 dirGuid虚拟目录Guid 等其他参数根据实际情况设置
        ///            
        ///         ④.(后台返回状态Statu，会话Session，文件名Name，Md5)
        ///            此时开始就是一个常规的分片上传了，一直反复在④和⑤持续，直到出现流程⑥为止
        ///            如果后台返回的是Statu为2，即断点续传，走前台需要走断点续传逻辑
        ///           
        ///         ⑤.(前端发现还有很多没有上传的分片，继续后续分片上传，发起请求)
        ///            此时的上传还是顺序提交，这样方便显示前端的进度条，根据chunk和maxChunk计算上传比例
        ///         
        ///            (经过一段时间上传后···)
        ///            
        ///         ⑥.(前端发现仅剩最后一片分片需上传时，前端发起最后一次请求)
        ///            将form表单中的statu设置为3表示即将结束当前文件上传，让后台准备好合并文件，其他form表单信息照旧
        ///            
        ///         ⑦.(后台根据前端表单状态得知已经全部上传完毕，并返回已经完成上传，并最终返回文件FileGuid)
        ///            后台去合并文件，并返回成功信息，即Statu设置为3
        ///            如果后台检测到文件还有遗漏，则会返回断点续传标识Statu=2，并返回遗漏分片集合，要求前台补传文件，此时会跳回到步骤④
        ///            
        ///         ⑧.(前端提示当前文件上传已完成)
        /// 
        /// 请前端程序在写上传有关代码时，注意以下：
        /// 
        ///     约定表单中的md5 必须一直都是第一个分片的md5，不是每个分片都要md5，切记！            注：实际上仅第一个分片提交表单时需要携带md5信息，后面分片不需要md5信息了
        ///     
        ///     约定每个分片大小必须相同，比如都是 2 MegaBytes，不然会造成分片校验的md5不一致！
        ///     
        ///     约定表单中的session必须由后台返回，且session每个分片上传时均必须在表单中携带session！         注：前端上传第一片文件时不需要携带session
        ///     
        ///     关于前端表单的statu 和 后台返回的Statu（注意区分大小写）
        ///         第一个分片提交时，设置表单中status为0，第一个分片提交完成后，根据返回的结果集的Statu（注意区分大小写），
        ///         判断后续业务处理逻辑（Statu 业务状态）：
        ///              0-(该状态由前端发起)初始上传:  表示开启一个新上传
        ///              1-(该状态由后台返回)常规上传:  前端得知此状态时，直接开始后续上传，并把表单中的statu设置为1，直到最后一片分片
        ///              2-(该状态由后台返回)断点续传:  后台会在第一个分片上传后，检测文件之前是否上传过但没有传完，同时返回给前端缺了哪些分片，前端根据此得知需断点续传哪些分片文件
        ///              3-(该状态由前端发起)最后分片:  前端通过设置最后一片分片的表单中statu为3，后台获取到statu为3时即可知道文件全部上传完毕
        ///              4-(该状态由后台返回)秒传成功:  前端根据此，直接跳过当前文件的后续分片上传业务，并直接拿到返回的文件FileGuid
        ///              
        ///         
        ///</remarks>
        /// <param name="fileDetail"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<ActionResult> UploadChunkFile([FromForm] ChunkFileUploadDetail fileDetail)
        {
            #region 入参检查
            if (fileDetail.name.IsNullOrWhiteSpace())
                return BadRequest("name 没有提供，无法上传！");
            if (fileDetail.file == null)
                return BadRequest("file 没有提供，无法上传！");
            if (fileDetail.statu != (int)UploadStatu.Init && (fileDetail.session.IsNullOrWhiteSpace() || !Guid.TryParse(fileDetail.session, out Guid session)))
                return BadRequest("session 不合法或没有提供，请检查！");
            if (fileDetail.md5.IsNullOrWhiteSpace() || (fileDetail.md5.Length != 32 && fileDetail.md5.Length != 16))
                return BadRequest("md5 不合法或没有提供，请检查！");
            if (fileDetail.chunkSize <= 0)
                return BadRequest("chunkSize(字节) 没有提供，无法上传！");
            if (fileDetail.fileSize <= 0)
                return BadRequest("fileSize(字节) 没有提供，无法上传！");
            if (fileDetail.chunk <= 0)
                return BadRequest("chunk 没有提供，无法上传！");
            if (fileDetail.maxChunk <= 0)
                return BadRequest("maxChunk 没有提供，无法上传！");
            if (fileDetail.chunk > fileDetail.maxChunk)
                return BadRequest("chunk 大于 maxChunk 没有意义，服务器拒绝上传！");
            if (fileDetail.file.Length > fileDetail.chunkSize)
                return BadRequest("file 长度大于分片大小，无法上传！");
            if (fileDetail.statu != (int)UploadStatu.Init && fileDetail.statu != (int)UploadStatu.ResumeUploading && fileDetail.chunk == 1)
                return BadRequest("statu 与 chunk 与上传业务流程不匹配，服务器拒绝上传！");
            if (fileDetail.path == "undefined" || fileDetail.path == "string")
                return BadRequest("path 没有正确提供，无法上传！");
            if (fileDetail.topDirGuid == "undefined" || fileDetail.topDirGuid == "string")
                return BadRequest("path 没有正确提供，无法上传！");
            #endregion

            var result = new FileUploadResult();

            #region 初始化上传
            if (fileDetail.statu == ((int)UploadStatu.Init))
            {
                #region 初始化·检查是否需要秒传业务
                var theSourceFile = await _fileService.FindOne(x => x.Md5 == fileDetail.md5 && x.FileSize == fileDetail.fileSize);

                if (theSourceFile != null)//找到相同的原始文件，进入秒传业务
                {
                    #region 检测临时文件夹
                    var tempDir = Path.Combine("StaticFiles", "TempFiles", theSourceFile.Id.ToString());
                    //如果存在临时目录，表示这个文件在上次上传流程中，没有上传完毕，需要断点续传，因为完整上传完成的文件会删除临时文件夹
                    if (Directory.Exists(tempDir))
                    {
                        //为原始文件补充上传：分片大小与总分片大小都等于1，且上次没有传输完毕，则这次成功上传会直接合并文件并返回成功
                        if (fileDetail.chunk == 1 && fileDetail.maxChunk == 1)
                        {
                            //直接合并单片文件为文件并保存
                            await UploadChunkAsync(fileDetail.file, theSourceFile.Id.ToString(), fileDetail.chunk);
                            await MergeFileAndUpdateFileInfAsync(tempDir, fileDetail.name, theSourceFile.Id.ToString());
                            //向映射文件存储一份文件信息拷贝
                            var newReflectFileGUID = await _reflectfileService.Insert(new ReflectFileInf
                            {
                                PId = theSourceFile.Id,
                                Name = Path.GetFileNameWithoutExtension(fileDetail.name),
                                Extension = Path.GetExtension(fileDetail.name),
                                AbstractDirPath = fileDetail.path,
                                AbstractTopDirGuid = fileDetail.topDirGuid,
                                Keep = false,
                            });
                            //返回给前端，告知已经上传完毕
                            result.FileGuid = newReflectFileGUID.AsGuid.ToString();
                            result.Statu = (int)UploadStatu.Finish;
                        }
                        //覆盖旧信息，返回前端要求前端断点续传缺失的文件
                        else
                        {
                            //存入第一个分片
                            await UploadChunkAsync(fileDetail.file, theSourceFile.Id.ToString(), fileDetail.chunk);
                            //检查缺失分片
                            CheckLackChunks(fileDetail.maxChunk, result.LackedChunks, tempDir);
                            //返回给前端，告知需要继续上传
                            result.Statu = (int)UploadStatu.ResumeUploading;
                        }
                        result.Session = theSourceFile.Id.ToString();
                        result.Name = fileDetail.name;
                        result.Md5 = fileDetail.md5;
                        result.Chunk = fileDetail.chunk;
                        return Json(result);
                    }
                    //找不到临时目录，那么文件应该是上传完毕并且合并完成了，也即实现了秒传
                    //但是在这之前还是检查一下合并文件是否存在
                    else
                    {
                        if (fileDetail.chunk == 1 && fileDetail.session.IsNullOrWhiteSpace())//这么写的原因是防止前台在chunk>1时，还设置状态为Init造成的反复添加新条目的错误
                        {
                            //当第一条分片来检查时，创建一条新文件记录
                            var newSameMd5FileGUID = await _reflectfileService.Insert(new ReflectFileInf
                            {
                                PId = theSourceFile.Id,
                                Name = Path.GetFileNameWithoutExtension(fileDetail.name),
                                Extension = Path.GetExtension(fileDetail.name),
                                AbstractTopDirGuid = fileDetail.topDirGuid,
                                AbstractDirPath = fileDetail.path,
                                Keep = false,
                            });
                            result.FileGuid = newSameMd5FileGUID.AsGuid.ToString();
                        }
                        result.Session = theSourceFile.Id.ToString();//单文件直接秒传成功不需要返回会话session，这么处理也方便前端判断业务是否正确
                        result.Name = fileDetail.name;
                        result.Md5 = fileDetail.md5;
                        result.Chunk = fileDetail.chunk;
                        result.Statu = (int)UploadStatu.RapidUpload;//返回秒传成功
                        return Json(result);
                    }
                    #endregion
                }
                #endregion

                #region 初始化·非秒传逻辑，常规上传
                else
                {
                    //未找到相同文件，那说明这是一个全新的文件，需要走正常上传流程即可，新增新条目，需创建一个新的会话，会话session可以用新文件的GUID标识
                    var newFileGUID = await _fileService.Insert(new FileInf
                    {
                        Name = Path.GetFileNameWithoutExtension(fileDetail.name),
                        Extension = Path.GetExtension(fileDetail.name),
                        MimeType = fileDetail.file.ContentType,
                        Md5 = fileDetail.md5,
                        FileSize = fileDetail.fileSize,
                        Keep = false,
                    });

                    //分片大小与总分片大小都等于1，则第一片上传成功立马合并文件
                    if (fileDetail.chunk == 1 && fileDetail.maxChunk == 1)
                    {
                        var tempDir = await UploadChunkAsync(fileDetail.file, newFileGUID.AsGuid.ToString(), fileDetail.chunk);
                        await MergeFileAndUpdateFileInfAsync(tempDir, fileDetail.name, newFileGUID.AsGuid.ToString());
                        //向映射文件存储一份文件信息拷贝
                        var newReflectFileGUID = await _reflectfileService.Insert(new ReflectFileInf
                        {
                            PId = newFileGUID.AsGuid,
                            Name = Path.GetFileNameWithoutExtension(fileDetail.name),
                            Extension = Path.GetExtension(fileDetail.name),
                            AbstractDirPath = fileDetail.path,
                            AbstractTopDirGuid = fileDetail.topDirGuid,
                            Keep = false,
                        });
                        //返回给前端，告知已经上传完毕
                        result.FileGuid = newReflectFileGUID.AsGuid.ToString();
                        result.Statu = (int)UploadStatu.Finish;
                    }
                    else//否则通知前台继续走新文件的其余分片上传流程
                    {
                        //存入第一个分片
                        await UploadChunkAsync(fileDetail.file, newFileGUID.AsGuid.ToString(), fileDetail.chunk);
                        //返回给前端，告知需要继续上传，同时返回会话id
                        result.Statu = (int)UploadStatu.Uploading;
                    }
                    result.Session = newFileGUID.AsGuid.ToString();
                    result.Name = fileDetail.name;
                    result.Md5 = fileDetail.md5;
                    result.Chunk = fileDetail.chunk;
                }
                #endregion

                return Json(result);
            }
            #endregion

            #region 普通上传
            if (fileDetail.statu == (int)UploadStatu.Uploading)
            {
                string tempDir = await UploadChunkAsync(fileDetail.file, fileDetail.session, fileDetail.chunk);

                //返回给前端，告知需要继续上传，通过md5和会话session区分文件
                result.Name = fileDetail.name;
                result.Session = fileDetail.session;
                result.Md5 = fileDetail.md5;
                result.Chunk = fileDetail.chunk;
                result.Statu = (int)UploadStatu.Uploading;
                return Json(result);
            }
            #endregion

            #region 断点续传
            if (fileDetail.statu == ((int)UploadStatu.ResumeUploading))
            {
                string tempDir = await UploadChunkAsync(fileDetail.file, fileDetail.session, fileDetail.chunk);

                //返回给前端，告知需要继续上传，通过md5和会话session区分文件
                result.Name = fileDetail.name;
                result.Session = fileDetail.session;
                result.Md5 = fileDetail.md5;
                result.Chunk = fileDetail.chunk;
                result.Statu = (int)UploadStatu.ResumeUploading;
                return Json(result);
            }
            #endregion

            #region 最后分片待合并
            //如果是最后一个分块， 则合并文件，并更新文件信息
            if (fileDetail.statu == ((int)UploadStatu.Finish))
            {
                //此时为避免出毛病，可以再检查一遍是否还缺少区块没上传，要求前端重传（断点续传）
                string tempDir = await UploadChunkAsync(fileDetail.file, fileDetail.session, fileDetail.chunk);
                //检查缺失分片
                CheckLackChunks(fileDetail.maxChunk, result.LackedChunks, tempDir);
                if (result.LackedChunks.Count > 0)
                    //返回给前端，告知需要继续上传
                    result.Statu = (int)UploadStatu.ResumeUploading;
                else
                {
                    await MergeFileAndUpdateFileInfAsync(tempDir, fileDetail.name, fileDetail.session);
                    //向映射文件存储一份文件信息拷贝
                    var newReflectFileGUID = await _reflectfileService.Insert(new ReflectFileInf
                    {
                        PId = Guid.Parse(fileDetail.session),
                        Name = Path.GetFileNameWithoutExtension(fileDetail.name),
                        Extension = Path.GetExtension(fileDetail.name),
                        AbstractDirPath = fileDetail.path,
                        AbstractTopDirGuid = fileDetail.topDirGuid,
                        Keep = false,
                    });
                    //返回给前端，告知已经上传完毕
                    result.FileGuid = newReflectFileGUID.AsGuid.ToString();
                    result.Statu = (int)UploadStatu.Finish;
                }
                result.Name = fileDetail.name;
                result.Session = fileDetail.session;
                result.Md5 = fileDetail.md5;
                result.Chunk = fileDetail.chunk;

                return Json(result);
            }
            #endregion

            //所有状态的业务条件都不满足，那么一定状态设置超出范围或者错误了
            throw new Exception("状态设置错误，业务中不允许");
        }

        /// <summary>
        /// 检查缺失的分片
        /// </summary>
        /// <param name="maxChunk">总区块数</param>
        /// <param name="LackedChunks">缺少的分片</param>
        /// <param name="tempDir">文件存储临时目录</param>
        private static void CheckLackChunks(int maxChunk, List<int> LackedChunks, string tempDir)
        {
            var files = Directory.GetFiles(tempDir);
            //排一下序，保证从0-N Write
            var fileParts = files.OrderBy(x => x.Length).ThenBy(x => x);
            for (int j = 1; j <= maxChunk; j++)
            {
                if (fileParts.FirstOrDefault(x => x.Split("\\").Last() == j.ToString()).IsNullOrWhiteSpace())
                {
                    LackedChunks.Add(j);
                }
            }
        }

        /// <summary>
        /// 合并文件并更新文件信息（仅新文件会进此逻辑）
        /// </summary>
        /// <param name="tempDir">文件存储临时目录</param>
        /// <param name="name">文件真实保存名称（含后缀）</param>
        /// <param name="guid">文件guid，也是新文件会产生的会话session</param>
        /// <returns></returns>
        private async Task MergeFileAndUpdateFileInfAsync(string tempDir, string name, string guid)
        {
            var fileFormat = Path.GetExtension(name);
            var yearMonth = DateTime.Now.ToString("yyyyMMdd");
            //最终保存完整合并文件的目录
            var finalDir = Path.Combine("StaticFiles", "Files", yearMonth);
            //判断文件夹是否存在，不存在则新建文件夹
            if (!FileOperateHelper.CreateFileDir(finalDir))
                throw new Exception($"路径：{finalDir}，创建失败！");

            //保存到真实目录时的文件名
            string saveName = guid + fileFormat;
            //最终保存完整合并文件的路径（含文件名）
            var finalPath = Path.Combine(finalDir, saveName);
            await MergeFileAfterUploadFinishAsync(finalPath, tempDir);

            //更新文件信息，实际上仅需要更新最终真实存储所在目录即可
            var fileInf = await _fileService.FindOne(x => x.Id == Guid.Parse(guid));
            fileInf.DirPath = finalDir;
            await _fileService.Update(fileInf);

           
        }

        /// <summary>
        /// 上传文件分片到临时文件夹（仅新文件会进此逻辑）
        /// </summary>
        /// <param name="file">前端上传的文件分片流</param>
        /// <param name="guid">上传文件的guid信息</param>
        /// <param name="index">第几块分片</param>
        /// <returns>返回临时文件夹目录</returns>
        private static async Task<string> UploadChunkAsync(IFormFile file, string guid, int index)
        {
            //临时保存分块的目录
            var tempDir = Path.Combine("StaticFiles", "TempFiles", guid);
            //判断文件夹是否存在，不存在则新建文件夹
            if (!FileOperateHelper.CreateFileDir(tempDir))
                throw new Exception($"路径：{tempDir}，创建失败！");

            //分块文件名为索引名，更严谨一些可以加上是否存在的判断，防止多线程时并发冲突，但是概率实在太低，故这里不做判断了，以后有空再修改
            var filePath = Path.Combine(tempDir, index.ToString());
            //表单中取得分片文件 并存储文件分片到临时目录（无后缀）
            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }
            return tempDir;
        }


        /// <summary>
        /// 最后分片上传完毕后，找到临时文件夹，并合并文件，随后删除临时文件夹
        /// </summary>
        /// <param name="finalPath">保存的最终路径（含文件名，该路径所在文件夹必须已经被创建过）</param>
        /// <param name="tempDir">分片文件所在的临时文件夹</param>
        /// <returns></returns>
        private static async Task MergeFileAfterUploadFinishAsync(string finalPath, string tempDir)
        {
            var files = Directory.GetFiles(tempDir);

            using (var fs = new FileStream(finalPath, FileMode.Create))
            {
                //排一下序，保证从0-N Write
                var fileParts = files.OrderBy(x => x.Length).ThenBy(x => x);
                foreach (var part in fileParts)
                {
                    var bytes = await System.IO.File.ReadAllBytesAsync(part);
                    await fs.WriteAsync(bytes, 0, bytes.Length);
                    bytes = null;
                    //删除分片
                    System.IO.File.Delete(part);
                }
                await fs.FlushAsync();
                fs.Close();
                //删除临时文件夹和分片文件
                Directory.Delete(tempDir);
            }
        }




    }


}
